// Make Shapes // Samples N random pixel positions from the first raster/image layer on the current spread, // reads the RGB colour at each position, and places a filled shape of that colour centred // on that pixel's location. All generated shapes are grouped into a single layer on completion. // Each shape gets independently randomised size, rotation, opacity and effect. // For 'Square', width and height use the same random value to stay proportional. // 'Random shape' = random closed polygon (3–20 straight segments) using width as max radius. // 'Curved shape' = random closed Bezier curve shape (3–20 curved segments) using width as max radius. // Rotation is applied around each shape's own centre point. // // Requires at least one raster or image layer on the current spread. 'use strict'; const { Document } = require('/document.js'); const { PixelReaderRGBA8 } = require('/pixelaccessor.js'); const { Shape, ShapeType } = require('/shapes.js'); const { ShapeNodeDefinition, PolyCurveNodeDefinition, ContainerNodeDefinition, NodeChildType } = require('/nodes.js'); const { FillDescriptor } = require('/fills.js'); const { RGBA8 } = require('/colours.js'); const { CurveBuilder, PolyCurve, Transform } = require('/geometry.js'); const { LineStyleDescriptor } = require('/linestyle.js'); const { AddChildNodesCommandBuilder, DocumentCommand, CompoundCommandBuilder, NodeMoveType } = require('/commands.js'); const { BevelEmbossLayerEffect, PhongBevelLayerEffect, OuterShadowLayerEffect } = require('/layereffects.js'); const { Selection } = require('/selections.js'); const { Dialog, DialogResult } = require('/dialog.js'); const { UnitType } = require('/units.js'); const { app } = require('/application.js'); //function addGrp(col, name) { // const g = col.addGroup(name); // g.enableSeparator = false; // return g; //} // Build a random closed polygon (straight segments) centred at (pcx, pcy) /* function makeRandomPolyCurve(pcx, pcy, maxLen) { const nodeCount = 3 + Math.floor(Math.random() * 18); const angleStep = (Math.PI * 2) / nodeCount; const points = []; for (let i = 0; i < nodeCount; i++) { const angle = angleStep * i + (Math.random() - 0.5) * angleStep * 0.5; const radius = maxLen * (0.3 + Math.random() * 0.7); points.push({ x: pcx + Math.cos(angle) * radius, y: pcy + Math.sin(angle) * radius }); } const cb = CurveBuilder.create(); cb.begin(points[0]); for (let i = 1; i < points.length; i++) cb.lineTo(points[i]); cb.close(); const pc = PolyCurve.create(); pc.addCurve(cb.createCurve()); return pc; } */ /* // Build a random closed Bezier curve shape centred at (pcx, pcy) function makeCurvedPolyCurve(pcx, pcy, maxLen) { const nodeCount = 3 + Math.floor(Math.random() * 18); const angleStep = (Math.PI * 2) / nodeCount; const points = []; for (let i = 0; i < nodeCount; i++) { const angle = angleStep * i + (Math.random() - 0.5) * angleStep * 0.4; const radius = maxLen * (0.3 + Math.random() * 0.7); points.push({ x: pcx + Math.cos(angle) * radius, y: pcy + Math.sin(angle) * radius }); } const cb = CurveBuilder.create(); cb.begin(points[0]); for (let i = 1; i <= points.length; i++) { const prev = points[(i - 1) % points.length]; const curr = points[i % points.length]; const dx = curr.x - prev.x, dy = curr.y - prev.y; const len = Math.sqrt(dx * dx + dy * dy) || 1; const curl = (Math.random() - 0.5) * maxLen * 0.6; const c1 = { x: prev.x + dx * 0.25 - (dy / len) * curl, y: prev.y + dy * 0.25 + (dx / len) * curl }; const c2 = { x: prev.x + dx * 0.75 - (dy / len) * curl, y: prev.y + dy * 0.75 + (dx / len) * curl }; if (i < points.length) cb.addBezier(c1, c2, curr); else cb.addBezier(c1, c2, points[0]); } cb.close(); const pc = PolyCurve.create(); pc.addCurve(cb.createCurve()); return pc; } */ const doc = Document.current; if (!doc) { app.alert('No active document.'); throw new Error('No document');} const spread = doc.currentSpread; let rasterNode = null; spread.layers.forEach(n => { if (!rasterNode && (n.isRasterNode || n.isImageNode)) rasterNode = n; }); if (!rasterNode) { app.alert('No raster or image layer found.\nPlease ensure a pixel or image layer exists on the spread.'); throw new Error('No raster image found'); } const RANDOM = 'random', CURVED = 'curved'; const shapeOptions = [ { label: 'Square', type: ShapeType.Rectangle, square: true }, { label: 'Rectangle', type: ShapeType.Rectangle, square: false }, { label: 'Circle / Ellipse', type: ShapeType.Ellipse, square: false }, { label: 'Triangle', type: ShapeType.Triangle, square: false }, { label: 'Diamond', type: ShapeType.Diamond, square: false }, { label: 'Pentagon', type: ShapeType.Polygon, square: false }, { label: 'Star', type: ShapeType.Star, square: false }, { label: 'Square Star', type: ShapeType.SquareStar, square: false }, { label: 'Heart', type: ShapeType.Heart, square: false }, { label: 'Crescent', type: ShapeType.Crescent, square: false }, { label: 'Cog', type: ShapeType.Cog, square: false }, { label: 'Cloud', type: ShapeType.Cloud, square: false }, { label: 'Arrow', type: ShapeType.Arrow, square: false }, { label: 'Trapezoid', type: ShapeType.Trapezoid, square: false }, ]; const effectOptions = [ 'No layer effect', '3D depth (up to 20px)', '3D depth 40 (Phong)', 'Random drop shadow', ]; const dlg = Dialog.create('Make Shapes'); const colL = dlg.addColumn(); //const colR = dlg.addColumn(); const g1 = colL.addGroup('Shapes'); const cmbShape = g1.addComboBox('Shape', shapeOptions.map(s => s.label), 0); const cmbEffect = g1.addComboBox('Layer effect', effectOptions, 0); const edtCount = g1.addUnitValueEditor('Number of shapes', UnitType.Number, UnitType.Number, 1000, 1, 100000); const edtMinWidth = g1.addUnitValueEditor( 'Min width (px)', UnitType.Pixel, UnitType.Pixel, 200, 1, 10000); const edtMaxWidth = g1.addUnitValueEditor( 'Max width (px)', UnitType.Pixel, UnitType.Pixel, 200, 1, 10000); const edtMinHeight = g1.addUnitValueEditor( 'Min height (px)', UnitType.Pixel, UnitType.Pixel, 200, 1, 10000); const edtMaxHeight = g1.addUnitValueEditor( 'Max height (px)', UnitType.Pixel, UnitType.Pixel, 200, 1, 10000); const edtRotMin = g1.addUnitValueEditor('Rotation min (\u00B0)', UnitType.Degree, UnitType.Degree, 0, -360, 360); const edtRotMax = g1.addUnitValueEditor('Rotation max (\u00B0)', UnitType.Degree, UnitType.Degree, 0, -360, 360); const edtOpacMin = g1.addUnitValueEditor( 'Opacity min (%)', UnitType.Percentage, UnitType.Percentage, 100, 0, 100); const edtOpacMax = g1.addUnitValueEditor( 'Opacity max (%)', UnitType.Percentage, UnitType.Percentage, 100, 0, 100); const redFactor = g1.addUnitValueEditor('Red Factor (0-2)', UnitType.Number, UnitType.Number, 1, 0, 2).setPrecision(3); const greenFactor = g1.addUnitValueEditor('Green Factor (0-2)', UnitType.Number, UnitType.Number, 1, 0, 2).setPrecision(3); const blueFactor = g1.addUnitValueEditor('Blue Factor (0-2)', UnitType.Number, UnitType.Number, 1, 0, 2).setPrecision(3); const result = dlg.runModal(); if (result.value !== DialogResult.Ok.value) return; const selected = shapeOptions[cmbShape.selectedIndex]; const effectIndex = cmbEffect.selectedIndex; const count = Math.round(edtCount.value); const isRandom = selected.type === RANDOM; const isCurved = selected.type === CURVED; const isPoly = isRandom || isCurved; let minW = edtMinWidth.value, maxW = edtMaxWidth.value; if (minW > maxW) { const t = minW; minW = maxW; maxW = t; } let minH = edtMinHeight.value, maxH = edtMaxHeight.value; if (minH > maxH) { const t = minH; minH = maxH; maxH = t; } let rotMin = edtRotMin.value, rotMax = edtRotMax.value; if (rotMin > rotMax) { const t = rotMin; rotMin = rotMax; rotMax = t; } let opacMin = edtOpacMin.value / 100, opacMax = edtOpacMax.value / 100; if (opacMin > opacMax) { const t = opacMin; opacMin = opacMax; opacMax = t; } const useRotation = rotMin !== 0 || rotMax !== 0; const useOpacity = opacMin !== 1 || opacMax !== 1; const useEffect = effectIndex > 0; doc.executeCommand(DocumentCommand.createSetCurrentSpread(spread)); const rw = rasterNode.rasterWidth; const rh = rasterNode.rasterHeight; const bb = rasterNode.getSpreadBaseBox(false); const red1 = redFactor; const green1 = greenFactor; const blue1 = blueFactor; const ri = rasterNode.rasterInterface; const bitmap = ri.createCompatibleBitmap(true); const reader = PixelReaderRGBA8.create(bitmap); const shape = Shape.create(selected.type); const lineStyle = null; const lineFill = null; const transFill = null; const placeBuilder = AddChildNodesCommandBuilder.create(); placeBuilder.setInsertionTarget(spread); for (let i = 0; i < count; i++) { const px = Math.floor(Math.random() * rw); const py = Math.floor(Math.random() * rh); const pixel = reader.readPixel(px, py); const fill = FillDescriptor.createSolid(RGBA8(pixel.r * redFactor.value, pixel.g*greenFactor.value, pixel.b*blueFactor.value, pixel.alpha)); const pcx = bb.x + (px / rw) * bb.width; const pcy = bb.y + (py / rh) * bb.height; // if (isPoly) { // const maxLen = minW + Math.random() * (maxW - minW); // const pc = isCurved ? makeCurvedPolyCurve(pcx, pcy, maxLen) : makeRandomPolyCurve(pcx, pcy, maxLen); // placeBuilder.addPolyCurveNode(PolyCurveNodeDefinition.create(pc, fill, lineStyle, lineFill, transFill)); //} else { const shapeW = minW + Math.random() * (maxW - minW); const shapeH = selected.square ? shapeW : minH + Math.random() * (maxH - minH); placeBuilder.addNode(ShapeNodeDefinition.create(shape, { x: pcx - shapeW / 2, y: pcy - shapeH / 2, width: shapeW, height: shapeH }, fill, null)); } //} const placeCmd = placeBuilder.createCommand(false, NodeChildType.Main); doc.executeCommand(placeCmd); const newNodes = []; placeCmd.newNodes.forEach(n => newNodes.push(n)); // Apply rotation, opacity and layer effects per node if (useRotation || useOpacity || useEffect) { const compound = CompoundCommandBuilder.create(); newNodes.forEach(node => { const sel = Selection.create(doc, node); if (useRotation) { const rotRad = (rotMin + Math.random() * (rotMax - rotMin)) * Math.PI / 180; const nodeBb = node.getSpreadBaseBox(false); const originX = nodeBb.x + nodeBb.width / 2; const originY = nodeBb.y + nodeBb.height / 2; let combined = Transform.createTranslate(-originX, -originY); combined = Transform.createRotate(rotRad).multiply(combined); combined = Transform.createTranslate(originX, originY).multiply(combined); compound.addCommand(DocumentCommand.createTransform(sel, combined, { mergeable: false })); } if (useOpacity) { const opacity = opacMin + Math.random() * (opacMax - opacMin); compound.addCommand(DocumentCommand.createSetOpacity(sel, opacity)); } if (useEffect && effectIndex === 1) { const effect = BevelEmbossLayerEffect.create(); effect.depth = Math.random() * 20; effect.radius = 2 + Math.random() * 13; effect.soften = Math.random() * 3; effect.azimuth = Math.random() * 360; effect.elevation = 20 + Math.random() * 50; compound.addCommand(DocumentCommand.createSetBevelEmbossLayerEffect(sel, effect)); } if (useEffect && effectIndex === 2) { const effect = PhongBevelLayerEffect.create(); effect.depth = Math.random() * 40; effect.radius = 3 + Math.random() * 15; effect.soften = Math.random() * 3; effect.ambient = 0.55 + Math.random() * 0.2; effect.diffuse = 0.5 + Math.random() * 0.5; effect.specular = Math.random() * 0.8; effect.shininess = 10 + Math.random() * 40; if (effect.lightCount > 0) { const light = effect.getLight(0); light.azimuth = Math.random() * 360; light.elevation = 20 + Math.random() * 50; } compound.addCommand(DocumentCommand.createSetPhongBevelLayerEffect(sel, effect)); } if (useEffect && effectIndex === 3) { const effect = OuterShadowLayerEffect.create(); effect.radius = 5 + Math.random() * 20; effect.offset = 5 + Math.random() * 30; effect.angle = Math.random() * Math.PI * 2; effect.intensity = 0.4 + Math.random() * 0.6; compound.addCommand(DocumentCommand.createSetOuterShadowLayerEffect(sel, effect)); } }); doc.executeCommand(compound.createCommand()); } // Group all generated shapes into a single container doc.executeCommand(DocumentCommand.createSetCurrentSpread(spread)); const grpBuilder = AddChildNodesCommandBuilder.create(); grpBuilder.setInsertionTarget(spread); grpBuilder.addContainerNode(ContainerNodeDefinition.create('Make Shapes Group')); const grpCmd = grpBuilder.createCommand(false, NodeChildType.Main); doc.executeCommand(grpCmd); const grpNodes = []; grpCmd.newNodes.forEach(n => grpNodes.push(n)); const container = grpNodes[0]; // Move each node individually into the container const moveCompound = CompoundCommandBuilder.create(); newNodes.forEach(node => { moveCompound.addCommand( DocumentCommand.createMoveNodes(Selection.create(doc, node), container, NodeMoveType.Inside, NodeChildType.Main) ); }); doc.executeCommand(moveCompound.createCommand());